Option Explicit

'################################################################
' Discriptor
' DF 向 sheet 写出数据时的上下文
'################################################################

'################################################################
' Field
'################################################################

'------------------------
' 基础数据
'------------------------
Private fieldWriteSheet As Worksheet
Private fieldDF As IDataFrame

' 可以写出的数据行数
Private fieldWritableRowCount As Long

' 可以写出的数据列数
Private fieldWritableColCount As Integer

' 向 sheet 写出列名的行号
' 如果不需要写出列名，则这个属性 = 0
Private fieldSheetColNameRowNo As Long

' 向 sheet 写出的数据的起始行号(不包括列列名)
Private fieldSheetDataStartRowNo As Long

' 向 sheet 写出的起始列号
Private fieldSheetStartColNo As Integer

' 是否输出了列名
Private fieldNeedColName As Boolean

' DF 中需要输出的数据的起始行索引
Private fieldDFRowStartIndex As Long
' DF 中需要输出的数据的结束行索引
Private fieldDFRowEndIndex As Long

' 行描述器的处理方法名
Private rowDescriptorHandlerMethodName As String
' 每个数据槽位的描述器的处理方法名
Private dataDescriptorHandlerMethodName As String
' 各列的 Format 字符串集合
Private fieldColFormatMap As Object
' 各列的 Format 字符串数组(为了在遍历 DF 数据并输入时加速)
' 无论有没有 Map，都创建一段数组
' 如果没有某个列的转换字符串，将会设置为空字符串: ""
Private fieldColFormatArray() As String
'------------------------
' (运行时的)活动索引
'------------------------
' 当前正在写出 DataFrame 的行的索引
Private fieldCurrentDFRowIndex As Long
' 当前正在写出 DataFrame 的列的索引
Private fieldCurrentDFColIndex As Integer

' 当前正在写出 Sheet 的行号
Private fieldCurrentSheetRowNo As Long
' 当前正在写出 Sheet 的列号
Private fieldCurrentSheetColNo As Integer

'################################################################
' Init
'################################################################

' 初始化
' @param sh 需要输出的 sheet 对象
' @param df 保存数据的 DataFrame 对象
' @param sheetWriteRowNo 输出到 sheet 的起始行号
' @param sheetWriteColNo 输出到 sheet 的起始列号
' @param needColName 是否需要输出 DataFrame 中的列名
' @param dfRowStart DataFrame 输出的起始行号
' @param dfRowEnd  DataFrame 输出的结束行号（包含这一行）
' @param rowDescriptorHandler 行处理器方法名
' @param dataDescriptorHandler 每个数据的处理器方法名
' @param colFormatMap 各列的 Format 字符串集合
'                     Map<ColIndex/"ColName", "Format String">
'                     如果没有设置当前参数，所有数据将会直接输出
Public Sub Init( _
    ByRef sh As Worksheet, _
    ByRef df As IDataFrame, _
    Optional ByRef sheetWriteRowNo As Variant = -1, _
    Optional ByRef sheetWriteColNo As Variant = -1, _
    Optional ByRef needColName As Boolean = True, _
    Optional ByRef dfRowStart As Long = -1, _
    Optional ByRef dfRowEnd As Long = -1, _
    Optional ByRef rowDescriptorHandler As String = "", _
    Optional ByRef dataDescriptorHandler As String = "", _
    Optional ByRef colFormatMap As Object _
    )

    ' 1. 检查参数
    ' 1.1 确认 DataFrame 输出的行索引
    Dim dfRowSize As Long
    dfRowSize = df.Size

    Dim startIdx As Long
    Dim endIdx As Long
    If dfRowSize = 0 Then
        ' 如果 df 中没有数据，则各索引设置为默认值
        startIdx = 0
        endIdx = -1
    Else
        ' 确认输出的起始行索引 + 结束行索引
        If dfRowStart < 0 Then
            startIdx = 0
        Else
            If dfRowStart >= dfRowSize Then
                startIdx = dfRowSize
            Else
                startIdx = dfRowStart
            End If
        End If

        If dfRowEnd < 0 Then
            endIdx = dfRowSize - 1
        Else
            If dfRowEnd >= dfRowSize Then
                endIdx = dfRowSize - 1
            Else
                endIdx = dfRowEnd
            End If
        End If
    End If

    ' 1.2 计算需要输出的行数
    Dim exceptedWritedRowCount As Long
    exceptedWritedRowCount = endIdx - startIdx + 1

    ' 1.3 将参数 sheetWriteRowNo + sheetWriteColNo 转换为正确的类型
    Dim sheetStartRowNo As Long
    Dim sheetStartColNo As Integer

    ' 如果行号小于 0，将会自动使用 1
    ' 如果行号大于最大值 1048576, 将会自动使用 1
On Error Goto castRowNo
    sheetStartRowNo = Int(sheetWriteRowNo)
    Goto castRowNoContinue
castRowNo:
    Err.Clear
    sheetStartRowNo = 1
castRowNoContinue:

    If sheetStartRowNo < 1 Then
        sheetStartRowNo = 1
    ElseIf sheetStartRowNo > UtilsWorksheet.MAX_ROW_NO Then
        sheetStartRowNo = 1
    End If

    ' 如果列号小于 0，将会自动使用 1
    ' 如果列号大于最大值 16384, 将会自动使用 1
    If VarType(sheetWriteColNo) = vbString Then
        sheetStartColNo = UtilsWorksheet.colStr2No(sheetWriteColNo & "")
    Else
On Error Goto castColNo
        sheetStartColNo = Int(sheetWriteColNo)
        Goto castColNoContinue
castColNo:
        Err.Clear
        sheetStartColNo = 1
castColNoContinue:
    End IF

    If sheetStartColNo < 1 Then
        sheetStartColNo = 1
    ElseIf sheetStartColNo > UtilsWorksheet.MAX_COL_NO Then
        sheetStartColNo = 1
    End If

    ' 1.3 确认最终能够输出到 sheet 中的行 + 列的数量
    Dim writableRowCount As Long
    Dim writableColCount As Integer

    ' 从指定的 sheet 行号开始，最多从 DataFrame 中读取并输出 (1048576 - 行号 + 1) 行，多余的行将被省略
    Dim writableMaxRowCount As Long
    writableMaxRowCount = UtilsWorksheet.MAX_ROW_NO - sheetStartRowNo + 1
    If needColName = True Then
        If writableMaxRowCount > exceptedWritedRowCount Then
            ' 这里可以保证至少可以有 writableMaxRowCount = exceptedWritedRowCount + 1
            ' 可以存在一行用来写出列名
            writableRowCount = exceptedWritedRowCount
        ElseIf writableMaxRowCount = exceptedWritedRowCount Then
            ' 这里只能少写一行数据，用来写出列名
            writableRowCount = exceptedWritedRowCount - 1
        Else
            ' 这里只能少写一行数据，用来写出列名
            writableRowCount = writableMaxRowCount - 1
        End If
    Else
        If writableMaxRowCount >= exceptedWritedRowCount Then
            writableRowCount = exceptedWritedRowCount
        Else
            writableRowCount = writableMaxRowCount
        End If
    End if

    ' 从指定的 sheet 列号开始，最多从 DataFrame 中读取并输出 (16384 - 行号 + 1) 列，多余的列将被省略
    Dim writableMaxColCount As Integer
    writableMaxColCount = UtilsWorksheet.MAX_COL_NO - sheetStartColNo + 1
    If writableMaxColCount >= df.ColumnCount Then
        writableColCount = df.ColumnCount
    Else
        writableColCount = writableMaxColCount
    End If

    ' 2. 保存基础参数
    ' 2.1 确定写入 sheet 的数据的起始行列号
    If needColName = True Then
        fieldSheetColNameRowNo = sheetStartRowNo
        fieldSheetDataStartRowNo = sheetStartRowNo + 1
    Else
        fieldSheetColNameRowNo = 0
        fieldSheetDataStartRowNo = sheetStartRowNo
    End If

    fieldSheetStartColNo = sheetStartColNo

    ' 2.2 保存其他基础参数
    Set fieldWriteSheet = sh
    Set fieldDF = df
    fieldWritableRowCount = writableRowCount
    fieldWritableColCount = writableColCount
    fieldDFRowStartIndex = startIdx
    fieldDFRowEndIndex = startIdx + writableRowCount - 1
    fieldNeedColName = needColName

    rowDescriptorHandlerMethodName = rowDescriptorHandler
    dataDescriptorHandlerMethodName = dataDescriptorHandler

    ' 生成 Format 字符串的数组
    Set fieldColFormatMap = colFormatMap

    ReDim Preserve fieldColFormatArray(0 To (fieldWritableColCount - 1))
    If Not fieldColFormatMap Is Nothing Then
        Dim j As Integer
        Dim colName As Variant

        For j = 0 To (fieldWritableColCount - 1)
            ' 获取列名
            colName = fieldDF.colIndexToName(j)

            ' 尝试获取当前列的 Format 字符串
            If fieldColFormatMap.Exists(colName) Then
                fieldColFormatArray(j) = fieldColFormatMap.Item(colName)
            ElseIf fieldColFormatMap.Exists(j) Then
                fieldColFormatArray(j) = fieldColFormatMap.Item(j)
            Else
                fieldColFormatArray(j) = ""
            End If
        Next
    End If
End Sub

'################################################################
' Property
'################################################################

'------------------------
' 获取基础数据
'------------------------
Public Property Get WriteSheet() As Worksheet
    Set WriteSheet = fieldWriteSheet
End Property
Public Property Get DataFrame() As IDataFrame
    Set DataFrame = fieldDF
End Property

' 获取可以写出的数据行数
Public Property Get WritableRowCount() As Long
    WritableRowCount = fieldWritableRowCount
End Property

' 可以写出的数据列数
Public Property Get WritableColCount() As Integer
    WritableColCount = fieldWritableColCount
End Property

' 向 sheet 写出列名的行号
Public Property Get SheetColNameRowNo() As Long
    SheetColNameRowNo = fieldSheetColNameRowNo
End Property

' 向 sheet 写出的起始行号(不包括列列名)
Public Property Get SheetDataStartRowNo() As Long
    SheetDataStartRowNo = fieldSheetDataStartRowNo
End Property

' 向 sheet 写出列名的行号
Public Property Get SheetStartColNo() As Integer
    SheetStartColNo = fieldSheetStartColNo
End Property

' 是否输出了列名
Public Property Get NeedColName() As Boolean
    NeedColName = fieldNeedColName
End Property

' DF 中需要输出的数据的起始行索引
Public Property Get DFRowStartIndex() As Long
    DFRowStartIndex = fieldDFRowStartIndex
End Property

' DF 中需要输出的数据的结束行索引
Public Property Get DFRowEndIndex() As Long
    DFRowEndIndex = fieldDFRowEndIndex
End Property

'------------------------
' 读写活动索引
'------------------------
' fieldCurrentDFRowIndex
' 获取当前正在写出 DataFrame 的行的索引
Public Property Get CurrentDFRowIndex() As Long
    CurrentDFRowIndex = fieldCurrentDFRowIndex
End Property

'------------------
' fieldCurrentDFColIndex
' 获取当前正在写出 DataFrame 的行的索引
Public Property Get CurrentDFColIndex() As Integer
    CurrentDFColIndex = fieldCurrentDFColIndex
End Property

'------------------
' fieldCurrentSheetRowNo
' 获取当前正在写出 Sheet 的行号
Public Property Get CurrentSheetRowNo() As Long
    CurrentSheetRowNo = fieldCurrentSheetRowNo
End Property

'------------------
' fieldCurrentSheetColNo
' 获取当前正在写出 Sheet 的列号
Public Property Get CurrentSheetColNo() As Integer
    CurrentSheetColNo = fieldCurrentSheetColNo
End Property

'################################################################
' Public Function
'################################################################
' 输出 DataFrame 中的数据
Public Sub writeDF()
    ' 初始化输入数据的区域
    initSheetWriteRange
    ' 输出列名(如果需要的话)
    writeColumnName
    ' 输出 DataFrame 中的数据
    writeData
End Sub

' 返回当前 sheet 中正在写入数据的活动单元格对象
Public Function activeSheetDataCell() As Range
    Set activeSheetDataCell = fieldWriteSheet.Cells(fieldCurrentSheetRowNo, fieldCurrentSheetColNo)
End Function

'################################################################
' Private Function
'################################################################
' 初始化 sheet 中需要写入数据的区域
'
' 清除整个区域的所有内容
' 将整个区域的格式设置为文本列行
Private Sub initSheetWriteRange()
    ' 确定起始行号
    Dim rangeStartRowNo As Long
    Dim rangeEndRowNo As Long
    If fieldNeedColName = True Then
        rangeStartRowNo = fieldSheetColNameRowNo
        rangeEndRowNo = fieldSheetColNameRowNo + fieldWritableRowCount
    Else
        rangeStartRowNo = fieldSheetDataStartRowNo
        rangeEndRowNo = fieldSheetDataStartRowNo + fieldWritableRowCount - 1
    End If

    ' 设置格式
    With fieldWriteSheet.Range( _
        fieldWriteSheet.Cells(rangeStartRowNo, fieldSheetStartColNo), _
        fieldWriteSheet.Cells(rangeEndRowNo, fieldSheetStartColNo + fieldWritableColCount - 1) _
    )
        .Clear
        .NumberFormatLocal = "@"
    End With
End Sub

' 向 sheet 中写出列名
Private Sub writeColumnName()
    If fieldNeedColName Then
        Dim i As Integer
        With fieldWriteSheet
            For i = 0 To (fieldWritableColCount - 1)
                ' 写出列名
                .Cells(fieldSheetColNameRowNo, fieldSheetStartColNo + i) = fieldDF.colIndexToName(i)
            Next
        End With
    End If
End Sub

' 向 sheet 中写出数据
Private Sub writeData()
    ' 检查: DF 中没有数据，则退出
    If fieldWritableRowCount = 0 Then
        Exit Sub
    End If

    ' 处理行描述器
    Dim i As Long
    If rowDescriptorHandlerMethodName <> "" Then
        For i = 0 To (fieldWritableRowCount - 1)
            fieldCurrentDFRowIndex = i
            fieldCurrentSheetColNo = fieldSheetStartColNo + i
            Application.Run rowDescriptorHandlerMethodName, Me, fieldDF.getRowDescriptor(i)
        Next
    End If

    Dim j As Integer
    Dim data As Variant
    Dim colSeries As IDFSeries
    Dim formatStr As String
    Dim colName As Variant
    Dim writedStr() As String
    ReDim writedStr(0 To fieldWritableRowCount - 1, 0)

    With fieldWriteSheet
        For j = 0 To (fieldWritableColCount - 1)
            ' 获取指定列对象，然后输出数据
            Set colSeries = fieldDF.getCol(j)

            ' 获取这一列的 Format 字符串
            formatStr = fieldColFormatArray(j)

            ' 保存当前活动列索引
            fieldCurrentDFColIndex = j
            fieldCurrentSheetColNo = fieldSheetStartColNo + j

            For i = 0 To (fieldWritableRowCount - 1)
                ' 保存当前活动行索引
                fieldCurrentDFRowIndex = i
                fieldCurrentSheetRowNo = fieldSheetDataStartRowNo + i

                ' 处理当前槽位的描述器
                If dataDescriptorHandlerMethodName <> "" Then
                    Application.Run dataDescriptorHandlerMethodName, Me, fieldDF.getDataDescriptor(i, j)
                End If

                ' 使用 Format 字符串转换数据
                data = colSeries.getData(i)
                If formatStr <> "" Then
                    data = Format(data, formatStr)
                End If

                ' 将这一列的数据保存到数组中
                writedStr(i,0) = data
            Next

            ' 将这一列的数据写入 sheet
            .Range( _
                .Cells(fieldSheetDataStartRowNo, fieldCurrentSheetColNo), _
                .Cells(fieldCurrentSheetRowNo, fieldCurrentSheetColNo) _
            ) = writedStr
        Next
    End With
End Sub
